Skip to main content

copp\copp\copp3\opt3/
topp3_socp.rs

1//! 3rd-order Time-Optimal Path Parameterization (TOPP3) based on second-order cone programming (SOCP).
2//!
3//! # Method identity
4//! This module implements the **optimization backend** for TOPP3-QP by transforming
5//! third-order path-parameterization constraints/objective into Clarabel-compatible
6//! conic form and solving with SOCP.
7//!
8//! # Discrete variables (local notation)
9//! On a path grid `s[0..=n]`:
10//! - `a[k]` denotes $\dot{s}_k^2$;
11//! - `b[k]` denotes $\ddot{s}_k$;
12//! - auxiliary variables `xi[k]` and `eta[k]` satisfy reciprocal-SOC coupling for
13//!   the time objective in QP form.
14//! - decision vector is organized as
15//!   `x = [a[0..=n], b[0..=n], xi[0..len_xi), eta[0..len_xi)]`.
16//!
17//! # High-level pipeline
18//! 1. Validate boundary/index contracts.
19//! 2. Assemble standard TOPP3 conic constraints.
20//! 3. Add QP-specific SOC constraints for `(xi, eta)` and reciprocal coupling.
21//! 4. Build sparse matrices `A`, `P`, vector `q`, and solve by Clarabel.
22//! 5. Apply status acceptance policy (`ClarabelOptions::is_allow`) and extract
23//!    `(a,b,num_stationary)` only when accepted.
24//!
25//! # API layering
26//! - `topp3_socp`: strict/normal API, returns only accepted `(a,b,num_stationary)`.
27//! - `topp3_socp_expert`: expert API returning `(Option<Copp3Result>, DefaultSolution<f64>)`.
28
29use crate::copp::clarabel_backend::ConstraintsClarabel;
30use crate::copp::copp3::Copp3Result;
31use crate::copp::copp3::formulation::{Topp3Problem, get_weight_a_topp3};
32use crate::copp::copp3::opt3::clarabel_constraints::{
33    clarabel_standard_capacity_topp3, clarabel_standard_constraint_topp3,
34};
35use crate::copp::{ClarabelOptions, clarabel_to_copp3_solution};
36use crate::diag::{
37    CoppError, DebugVerboser, SilentVerboser, SummaryVerboser, TraceVerboser, Verboser, Verbosity,
38    check_boundary_state_copp3_valid, check_s_interval_valid, format_duration_human,
39};
40use clarabel::algebra::CscMatrix;
41use clarabel::solver::SupportedConeT::{NonnegativeConeT, SecondOrderConeT};
42use clarabel::solver::{DefaultSolution, DefaultSolver, IPSolver, SupportedConeT};
43
44/// Strict TOPP3-SOCP API for production use.
45///
46/// # Purpose
47/// Use this entry when caller only needs a valid profile `(a,b,num_stationary)` and treats
48/// non-accepted solver statuses as hard failures.
49///
50/// # Contract
51/// - Internally calls [`topp3_socp_expert`].
52/// - Returns `Ok((a,b,num_stationary))` **iff** `options.is_allow(solution.status)` is `true`.
53/// - Returns `Err(CoppError::ClarabelSolverStatus(...))` when status is not accepted.
54///
55/// # Returns
56/// Returns accepted TOPP3 profile `(a, b, num_stationary)`.
57///
58/// # Errors
59/// Returns [`CoppError`] on conic-model/solver failures and non-accepted solver status.
60///
61/// More details are provided in the documentation of [`topp3_socp_expert`].
62pub fn topp3_socp(
63    problem: &Topp3Problem,
64    options: &ClarabelOptions,
65) -> Result<Copp3Result, CoppError> {
66    let (result, solution) = topp3_socp_expert(problem, options)?;
67    result.ok_or_else(|| CoppError::ClarabelSolverStatus("topp3_socp".into(), solution.status))
68}
69
70/// Expert TOPP3-SOCP API with full Clarabel solution exposure.
71///
72/// # Return contract
73/// - `Ok((Some(result), solution))`: status accepted by `options.is_allow(solution.status)`.
74/// - `Ok((None, solution))`: solve finished but status not accepted.
75/// - `Err(...)`: input/model/solver-construction runtime failures.
76///
77/// # Returns
78/// Returns tuple `(Option<Copp3Result>, DefaultSolution<f64>)` for diagnostic pipelines.
79///
80/// # Errors
81/// Returns [`CoppError`] only for true build/runtime failures.
82///
83/// # Contract
84/// - caller handles `None` profile when status is not accepted;
85/// - acceptance policy is controlled by `options.is_allow`.
86///
87/// # Verbosity behavior
88/// Logging is layered by `options.verbosity()`:
89/// - [`Silent`](Verbosity::Silent): no algorithm logs;
90/// - [`Summary`](Verbosity::Summary): lifecycle milestones and elapsed time;
91/// - [`Debug`](Verbosity::Debug): assembly-level counters and stage summaries;
92/// - [`Trace`](Verbosity::Trace): fine-grained stage deltas and solver snapshot diagnostics.
93pub fn topp3_socp_expert(
94    problem: &Topp3Problem,
95    options: &ClarabelOptions,
96) -> Result<(Option<Copp3Result>, DefaultSolution<f64>), CoppError> {
97    match options.verbosity() {
98        Verbosity::Silent => topp3_socp_core(problem, (options, SilentVerboser)),
99        Verbosity::Summary => topp3_socp_core(problem, (options, SummaryVerboser::new())),
100        Verbosity::Debug => topp3_socp_core(problem, (options, DebugVerboser::new())),
101        Verbosity::Trace => topp3_socp_core(problem, (options, TraceVerboser::new())),
102    }
103}
104
105/// Core implementation for TOPP3-SOCP expert flow.
106///
107/// # Internal contract
108/// `options_verboser` packs:
109/// - `options`: acceptance policy and Clarabel numerical settings;
110/// - `verboser`: concrete logger implementation chosen by external verbosity dispatch.
111///
112/// # Invariants
113/// - decision-variable layout always starts with contiguous `a[0..=n]` and `b[0..=n]`;
114/// - auxiliary block `[xi, eta]` has shared length `length_xi_eta(n, num_stationary)`;
115/// - extracted `(a,b)` is produced only through `clarabel_to_copp3_solution` when status is accepted.
116fn topp3_socp_core(
117    problem: &Topp3Problem,
118    options_verboser: (&ClarabelOptions, impl Verboser),
119) -> Result<(Option<Copp3Result>, DefaultSolution<f64>), CoppError> {
120    let (options, mut verboser) = options_verboser;
121    let idx_s_start = problem.idx_s_start;
122    let a_boundary = problem.a_boundary;
123    let b_boundary = problem.b_boundary;
124    let num_stationary = problem.num_stationary;
125    if verboser.is_enabled(Verbosity::Summary) {
126        verboser.record_start_time();
127    }
128    if verboser.is_enabled(Verbosity::Trace) {
129        let settings = options.clarabel_settings();
130        crate::verbosity_log!(
131            crate::diag::Verbosity::Summary,
132            "topp3_socp: options snapshot -> allow(almost={}, max_iter={}, max_time={}, callback_term={}, insufficient_progress={}), tol_gap_rel={}, tol_feas={}, max_iter={}, verbose={}",
133            options.is_allow(clarabel::solver::SolverStatus::AlmostSolved),
134            options.is_allow(clarabel::solver::SolverStatus::MaxIterations),
135            options.is_allow(clarabel::solver::SolverStatus::MaxTime),
136            options.is_allow(clarabel::solver::SolverStatus::CallbackTerminated),
137            options.is_allow(clarabel::solver::SolverStatus::InsufficientProgress),
138            settings.tol_gap_rel,
139            settings.tol_feas,
140            settings.max_iter,
141            settings.verbose
142        );
143    }
144
145    // Check input validity
146    check_boundary_state_copp3_valid(a_boundary, b_boundary)?;
147    let n = problem.a_linearization.len() - 1;
148    let idx_s_final = idx_s_start + n;
149    if verboser.is_enabled(Verbosity::Summary) {
150        crate::verbosity_log!(
151            crate::diag::Verbosity::Summary,
152            "\ntopp3_socp started: {} <= idx_s <= {}, s_len = {}, num_stationary={:?}.",
153            idx_s_start,
154            idx_s_final,
155            problem.a_linearization.len(),
156            num_stationary
157        );
158    }
159    check_s_interval_valid("topp3_socp", idx_s_start, idx_s_final)?;
160    let len_xi = length_xi_eta(n, num_stationary);
161    let id_xi_start = 2 * (n + 1);
162    let id_eta_start = id_xi_start + len_xi;
163    // Let x = [a[0,1,...,n],
164    //          b[0,1,...,n],
165    //          xi[0,1,...,len_xi-1],
166    //          eta[0,1,...,len_xi-1]]
167    //       \in R^{2*(n+1)+2*len_xi}.
168    // Step 1. Deal with constraints
169    // s=b-A*x \in cone, where A[row[i],col[i]]=val[i], A \in R^{m*(n+1)}, b \in R^m, s \in R^m
170    // -s=-b+A*x
171    // Step 1.1 create constraints
172    let (cap_val_lp, cap_b_lp, cap_cone_lp) =
173        clarabel_standard_capacity_topp3(problem.constraints, (idx_s_start, idx_s_final));
174    let (cap_val_qp, cap_b_qp, cap_cone_qp) = clarabel_capacity_topp3_qp(n);
175    if verboser.is_enabled(Verbosity::Debug) {
176        crate::verbosity_log!(
177            crate::diag::Verbosity::Summary,
178            "topp3_socp: capacity estimate lp(val={cap_val_lp}, b={cap_b_lp}, cone={cap_cone_lp}), qp(val={cap_val_qp}, b={cap_b_qp}, cone={cap_cone_qp}), n_var={}",
179            id_eta_start + len_xi
180        );
181    }
182    let mut cones = Vec::<SupportedConeT<f64>>::with_capacity(cap_cone_lp + cap_cone_qp);
183    let mut row = Vec::<usize>::with_capacity(cap_val_lp + cap_val_qp);
184    let mut col = Vec::<usize>::with_capacity(cap_val_lp + cap_val_qp);
185    let mut val = Vec::<f64>::with_capacity(cap_val_lp + cap_val_qp);
186    let mut b = Vec::<f64>::with_capacity(cap_b_lp + cap_b_qp);
187    if verboser.is_enabled(Verbosity::Trace) {
188        crate::verbosity_log!(
189            crate::diag::Verbosity::Summary,
190            "topp3_socp: allocated capacities row/col/val/b/cones <= {}/{}/{}/{}/{}",
191            cap_val_lp + cap_val_qp,
192            cap_val_lp + cap_val_qp,
193            cap_val_lp + cap_val_qp,
194            cap_b_lp + cap_b_qp,
195            cap_cone_lp + cap_cone_qp
196        );
197    }
198
199    // Step 1.2 deal with standard constraints
200    let s = problem.constraints.s_vec(idx_s_start, idx_s_final + 1)?;
201    let row_before_std = row.len();
202    let col_before_std = col.len();
203    let val_before_std = val.len();
204    let b_before_std = b.len();
205    let cones_before_std = cones.len();
206    clarabel_standard_constraint_topp3(
207        problem,
208        &s,
209        (&mut row, &mut col, &mut val, &mut b, &mut cones),
210        num_stationary,
211        &verboser,
212    )?;
213    if verboser.is_enabled(Verbosity::Trace) {
214        crate::verbosity_log!(
215            crate::diag::Verbosity::Summary,
216            "topp3_socp: standard-constraints delta row/col/val/b/cones = +{}/+{}/+{}/+{}/+{}",
217            row.len() - row_before_std,
218            col.len() - col_before_std,
219            val.len() - val_before_std,
220            b.len() - b_before_std,
221            cones.len() - cones_before_std
222        );
223    }
224    // Step 1.3 deal with additional constraints for QP
225    let row_before_qp = row.len();
226    let col_before_qp = col.len();
227    let val_before_qp = val.len();
228    let b_before_qp = b.len();
229    let cones_before_qp = cones.len();
230    clarabel_constraint_topp3_qp(
231        (&mut row, &mut col, &mut val, &mut b, &mut cones),
232        (idx_s_start, idx_s_final),
233        num_stationary,
234        id_xi_start,
235        id_eta_start,
236    );
237    if verboser.is_enabled(Verbosity::Trace) {
238        crate::verbosity_log!(
239            crate::diag::Verbosity::Summary,
240            "topp3_socp: qp-aux delta row/col/val/b/cones = +{}/+{}/+{}/+{}/+{}",
241            row.len() - row_before_qp,
242            col.len() - col_before_qp,
243            val.len() - val_before_qp,
244            b.len() - b_before_qp,
245            cones.len() - cones_before_qp
246        );
247    }
248
249    // Step 1.4 build the constraints
250    let n_var = id_eta_start + len_xi;
251    let row_len = row.len();
252    let col_len = col.len();
253    let val_len = val.len();
254    let b_len = b.len();
255    let cones_len = cones.len();
256    let a_csc = CscMatrix::new_from_triplets(b.len(), n_var, row, col, val);
257    // Step 2. objective function (time QP surrogate): min \sum w[k] * eta[k]
258    let p_object = CscMatrix::<f64>::zeros((n_var, n_var));
259    let q_object = clarabel_q_object_topp3_qp(&s, num_stationary, n_var, id_eta_start);
260    if verboser.is_enabled(Verbosity::Trace) {
261        let (q_min, q_max) = q_object
262            .iter()
263            .fold((f64::INFINITY, f64::NEG_INFINITY), |(mn, mx), &v| {
264                (mn.min(v), mx.max(v))
265            });
266        crate::verbosity_log!(
267            crate::diag::Verbosity::Summary,
268            "topp3_socp: matrix built with m={}, n={}, A.nnz={}, P.nnz={}, q_range=[{}, {}]",
269            b_len,
270            n_var,
271            a_csc.nnz(),
272            p_object.nnz(),
273            q_min,
274            q_max
275        );
276    }
277    if verboser.is_enabled(Verbosity::Summary) {
278        crate::verbosity_log!(
279            crate::diag::Verbosity::Summary,
280            "topp3_socp: ready to solve with row/col/val/b/cones = {row_len}/{col_len}/{val_len}/{b_len}/{cones_len} and n_var = {n_var}.",
281        );
282    }
283    // Step 3. solve the SOCP problem
284    let settings = options.clarabel_settings().clone();
285    let mut solver = DefaultSolver::<f64>::new(&p_object, &q_object, &a_csc, &b, &cones, settings)
286        .map_err(|e| CoppError::ClarabelSolverError("topp3_socp".into(), e))?;
287    solver.solve();
288    let solution = solver.solution;
289    if verboser.is_enabled(Verbosity::Summary) {
290        crate::verbosity_log!(
291            crate::diag::Verbosity::Summary,
292            "topp3_socp: solve done, status = {:?}, elapsed = {}.",
293            solution.status,
294            format_duration_human(verboser.elapsed())
295        );
296    }
297    if verboser.is_enabled(Verbosity::Trace) {
298        let show = solution.x.len().min(3);
299        crate::verbosity_log!(
300            crate::diag::Verbosity::Summary,
301            "topp3_socp: solution x_len={}, head={:?}",
302            solution.x.len(),
303            &solution.x[0..show]
304        );
305    }
306    let result = if options.is_allow(solution.status) {
307        let (a, b) =
308            clarabel_to_copp3_solution(&solution.x.as_slice()[0..2 * (n + 1)], &s, num_stationary);
309        Some((a, b, num_stationary))
310    } else {
311        None
312    };
313    if verboser.is_enabled(Verbosity::Trace) {
314        crate::verbosity_log!(
315            crate::diag::Verbosity::Summary,
316            "topp3_socp: allow(status)={}, extracted_profile={}",
317            options.is_allow(solution.status),
318            if result.is_some() {
319                "Some((a,b,num_stationary))"
320            } else {
321                "None"
322            }
323        );
324    }
325    Ok((result, solution))
326}
327
328/// Determine the length of `xi` and `eta` in the decision variable `x`.
329#[inline(always)]
330fn length_xi_eta(n: usize, num_stationary: (usize, usize)) -> usize {
331    n + 1 - num_stationary.0.max(1) - num_stationary.1.max(1)
332}
333
334/// Return `k_skip`, where `eta[k] = 1/sqrt(a[k + k_skip])`.
335#[inline(always)]
336fn skip_a_for_xi(num_stationary_start: usize) -> usize {
337    num_stationary_start.max(1)
338}
339
340/// Create the constraints for clarabel TOPP3-QP.  
341/// `idx_s_interval`: (idx_s_start, idx_s_final), the interval of s for which we want to compute the time-optimal profile.  
342/// `num_stationary`: (num_stationary_start, num_stationary_final), the number of stationary points at the start and final of the interval.  
343/// `id_xi_start`: the starting index of xi in the decision variable x.
344/// `id_eta_start`: the starting index of eta in the decision variable x.
345fn clarabel_constraint_topp3_qp(
346    constraints: ConstraintsClarabel,
347    idx_s_interval: (usize, usize),
348    num_stationary: (usize, usize),
349    id_xi_start: usize,
350    id_eta_start: usize,
351) {
352    let (idx_s_start, idx_s_final) = idx_s_interval;
353    let n = idx_s_final - idx_s_start;
354    // s=b-A*x \in cone, where A[row[i],col[i]]=val[i]
355    // -s=-b+A*x
356    let (row, col, val, b, cones) = constraints;
357    // Add constraints for xi and eta
358    // xi[k] >= 0, eta[k] >= 0
359    // norm2([2, xi[k] - eta[k]]) <= xi[k] + eta[k]
360    // xi[k] * xi[k] <= a[k + k_skip]
361    let len_xi = length_xi_eta(n, num_stationary);
362    let k_skip = skip_a_for_xi(num_stationary.0);
363    // Step 1. xi[i] >= 0
364    // A*x-b = -s = -1*xi[k] <= 0
365    row.extend(b.len()..(b.len() + len_xi));
366    col.extend(id_xi_start..(id_xi_start + len_xi));
367    val.resize(val.len() + len_xi, -1.0);
368    b.resize(b.len() + len_xi, 0.0);
369    // Step 2. eta[i] >= 0
370    // A*x-b = -s = -1*eta[k] <= 0
371    row.extend(b.len()..(b.len() + len_xi));
372    col.extend(id_eta_start..(id_eta_start + len_xi));
373    val.resize(val.len() + len_xi, -1.0);
374    b.resize(b.len() + len_xi, 0.0);
375    cones.push(NonnegativeConeT(2 * len_xi));
376    // Step 3. norm2([2, xi[k] - eta[k]]) <= xi[k] + eta[k]
377    // -A*x+b = s = [xi[k] + eta[k], xi[k] - eta[k], 2] \in SOC
378    for k in 0..len_xi {
379        // xi[k] + eta[k]
380        row.resize(row.len() + 2, b.len());
381        col.extend([id_xi_start + k, id_eta_start + k]);
382        val.extend([-1.0, -1.0]);
383        b.push(0.0);
384        // xi[k] - eta[k]
385        row.resize(row.len() + 2, b.len());
386        col.extend([id_xi_start + k, id_eta_start + k]);
387        val.extend([-1.0, 1.0]);
388        b.push(0.0);
389        // 2
390        b.push(2.0);
391    }
392    // Step 4. xi[k] * xi[k] <= a[k_skip + k]
393    // norm2([2*xi[k], a[k_skip + k] - 1]) <= a[k_skip + k] + 1
394    // -A*x+b = s = [a[k_skip + k] + 1, a[k_skip + k] - 1, 2*xi[k]] \in SOC
395    for k in 0..len_xi {
396        // a[k_skip + k] + 1
397        row.push(b.len());
398        col.push(k_skip + k);
399        val.push(-1.0);
400        b.push(1.0);
401        // a[k_skip + k] - 1
402        row.push(b.len());
403        col.push(k_skip + k);
404        val.push(-1.0);
405        b.push(-1.0);
406        // 2*xi[k]
407        row.push(b.len());
408        col.push(id_xi_start + k);
409        val.push(-2.0);
410        b.push(0.0);
411    }
412    cones.resize(cones.len() + 2 * len_xi, SecondOrderConeT(3));
413}
414
415/// Build the linear objective coefficient `q` for TOPP3-QP.
416#[inline(always)]
417fn clarabel_q_object_topp3_qp(
418    s: &[f64],
419    num_stationary: (usize, usize),
420    n_var: usize,
421    id_eta_start: usize,
422) -> Vec<f64> {
423    let mut q_object = Vec::<f64>::with_capacity(n_var);
424    let weight = get_weight_a_topp3(s, num_stationary);
425    let len_eta = length_xi_eta(s.len() - 1, num_stationary);
426    let k_skip = skip_a_for_xi(num_stationary.0);
427    q_object.resize(id_eta_start, 0.0);
428    q_object.extend(weight[k_skip..(k_skip + len_eta)].iter());
429    q_object.resize(n_var, 0.0);
430    q_object
431}
432
433/// Determine Clarabel pre-allocation capacity for TOPP3-QP auxiliary constraints.
434///
435/// Returns `(capacity_val, capacity_b, capacity_cones)` as upper bounds.
436#[inline(always)]
437fn clarabel_capacity_topp3_qp(n: usize) -> (usize, usize, usize) {
438    // Step 1. xi[k] >= 0, eta[k] >= 0
439    //         (num_val==2*len_xi; num_b==2*len_xi, num_cone==1)
440    // Step 2. [2, xi[k] - eta[k], xi[k] + eta[k]] \in SOC
441    //         (num_val==4*len_xi; num_b==3*len_xi, num_cone==len_xi)
442    // Step 3. [2*xi[k], a[num_stationary.0 + k] - 1, a[num_stationary.0 + k] + 1] \in SOC
443    //         (num_val==3*len_xi; num_b==3*len_xi, num_cone==len_xi)
444    // len_xi = n + 1 - num_stationary.0 - num_stationary.1 <= n + 1
445    let len_xi_upper_bound = n + 1;
446    (
447        9 * len_xi_upper_bound,
448        8 * len_xi_upper_bound,
449        2 * len_xi_upper_bound + 1,
450    )
451}
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456    use crate::copp::copp2::stable::basic::{Topp2ProblemBuilder, s_to_t_topp2};
457    use crate::copp::copp2::stable::reach_set2::{ReachSet2Options, ReachSet2OptionsBuilder};
458    use crate::copp::copp2::stable::topp2_ra::topp2_ra;
459    use crate::copp::copp3::stable::basic::{Topp3ProblemBuilder, s_to_t_topp3};
460    use crate::copp::{ClarabelOptions, ClarabelOptionsBuilder};
461    use crate::path::add_symmetric_axial_limits_for_test;
462    use crate::robot::robot_core::Robot;
463    use crate::solver::topp3_lp::topp3_lp;
464    use core::f64;
465    use nalgebra::DMatrix;
466    use rand::RngExt;
467    use std::time::Instant;
468
469    #[test]
470    fn test_topp3_lp_qp() -> Result<(), CoppError> {
471        run_test_topp3_lp_qp_repeated(1, false)
472    }
473
474    /// Conditions: release, --include-ignored, CPU = Intel(R) Core(TM) Ultra 9 285K.
475    /// AAverage (fail 1): tc_ra = 0.3294 ms, tc_lp = 257.8678 ms, tc_qp = 327.9065 ms, tf_ra = 6.1838, tf_lp = 7.1079, tf_qp = 7.1079
476    #[test]
477    #[ignore = "slow"]
478    fn test_topp3_lp_qp_robust() -> Result<(), CoppError> {
479        run_test_topp3_lp_qp_repeated(100, true)
480    }
481
482    fn run_one_topp3_lp_qp_case(
483        options_ra: &ReachSet2Options,
484        options_lp: &ClarabelOptions,
485        options_qp: &ClarabelOptions,
486    ) -> Result<(f64, f64, f64, f64, f64, f64), CoppError> {
487        let n: usize = 1000;
488        let dim = 7;
489        let mut rng = rand::rng();
490        let omega = (0..dim)
491            .map(|_| rng.random_range(0.1..(2.0 * f64::consts::PI)))
492            .collect::<Vec<f64>>();
493        let phi = (0..dim)
494            .map(|_| rng.random_range(0.0..(2.0 * f64::consts::PI)))
495            .collect::<Vec<f64>>();
496
497        let mut robot = Robot::with_capacity(dim, n);
498        let s = DMatrix::<f64>::from_fn(1, n, |_, j| {
499            (j as f64
500                + (if 0 < j && 2 * j < n { 0.5 } else { 0.0 }
501                    + if n > j && 2 * j > n { 0.5 } else { 0.0 })
502                    * j as f64
503                    / n as f64)
504                * (1.0 / (n - 1) as f64)
505        });
506        let q = DMatrix::<f64>::from_fn(dim, n, |i, j| (omega[i] * s[j] + phi[i]).sin());
507        let dq =
508            DMatrix::<f64>::from_fn(dim, n, |i, j| omega[i] * (omega[i] * s[j] + phi[i]).cos());
509        let ddq = DMatrix::<f64>::from_fn(dim, n, |i, j| {
510            -omega[i] * omega[i] * (omega[i] * s[j] + phi[i]).sin()
511        });
512        let dddq = DMatrix::<f64>::from_fn(dim, n, |i, j| {
513            -omega[i] * omega[i] * omega[i] * (omega[i] * s[j] + phi[i]).cos()
514        });
515        robot.with_s(&s.as_view())?;
516        robot.with_q(
517            &q.as_view(),
518            &dq.as_view(),
519            &ddq.as_view(),
520            Some(&dddq.as_view()),
521            0,
522        )?;
523        add_symmetric_axial_limits_for_test(&mut robot, 1.0, 1.0, Some(5.0))?;
524
525        let topp2_problem = Topp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0)).build()?;
526        let start = Instant::now();
527        let a_ra = topp2_ra(&topp2_problem, options_ra)?;
528        let tc_ra = start.elapsed().as_secs_f64() * 1E3;
529        let (tf_ra, _) = s_to_t_topp2(s.as_slice(), &a_ra, 0.0);
530
531        robot.constraints.amax_substitute(&a_ra, 0)?;
532        let topp3_problem = Topp3ProblemBuilder::new(&mut robot, 0, &a_ra, (0.0, 0.0), (0.0, 0.0))
533            .with_num_stationary_max(2)
534            .build_with_linearization()?;
535
536        let start = Instant::now();
537        let (a_lp, b_lp, num_stationary) = topp3_lp(&topp3_problem, options_lp)?;
538        let tc_lp = start.elapsed().as_secs_f64() * 1E3;
539        let (tf_lp, _) = s_to_t_topp3(s.as_slice(), &a_lp, &b_lp, num_stationary, 0.0);
540
541        let start = Instant::now();
542        let (a_qp, b_qp, num_stationary_qp) = topp3_socp(&topp3_problem, options_qp)?;
543        let tc_qp = start.elapsed().as_secs_f64() * 1E3;
544        let (tf_qp, _) = s_to_t_topp3(s.as_slice(), &a_qp, &b_qp, num_stationary_qp, 0.0);
545
546        Ok((tc_ra, tc_lp, tc_qp, tf_ra, tf_lp, tf_qp))
547    }
548
549    fn run_test_topp3_lp_qp_repeated(n_exp: usize, flag_print_step: bool) -> Result<(), CoppError> {
550        let options_ra = ReachSet2OptionsBuilder::new()
551            .lp_feas_tol(1E-9)
552            .a_cmp_abs_tol(1E-9)
553            .a_cmp_rel_tol(1E-9)
554            .build()?;
555        let options_lp = ClarabelOptionsBuilder::new()
556            .allow_almost_solved(true)
557            .build()?;
558        let options_qp = ClarabelOptionsBuilder::new()
559            .allow_almost_solved(true)
560            .build()?;
561
562        let mut tc_sum_ra = 0.0;
563        let mut tc_sum_lp = 0.0;
564        let mut tc_sum_qp = 0.0;
565        let mut tf_sum_ra = 0.0;
566        let mut tf_sum_lp = 0.0;
567        let mut tf_sum_qp = 0.0;
568        let mut succeed = 0;
569
570        for i_exp in 0..n_exp {
571            if let Ok((tc_ra, tc_lp, tc_qp, tf_ra, tf_lp, tf_qp)) =
572                run_one_topp3_lp_qp_case(&options_ra, &options_lp, &options_qp)
573            {
574                if flag_print_step {
575                    crate::verbosity_log!(
576                        crate::diag::Verbosity::Summary,
577                        "Exp #{}: tc_ra = {:.4} ms, tc_lp = {:.4} ms, tc_qp = {:.4} ms, tf_ra = {:.4}, tf_lp = {:.4}, tf_qp = {:.4}",
578                        i_exp + 1,
579                        tc_ra,
580                        tc_lp,
581                        tc_qp,
582                        tf_ra,
583                        tf_lp,
584                        tf_qp,
585                    );
586                }
587                tc_sum_ra += tc_ra;
588                tc_sum_lp += tc_lp;
589                tc_sum_qp += tc_qp;
590                tf_sum_ra += tf_ra;
591                tf_sum_lp += tf_lp;
592                tf_sum_qp += tf_qp;
593                succeed += 1;
594            }
595        }
596
597        crate::verbosity_log!(
598            crate::diag::Verbosity::Summary,
599            "Average (fail {}): tc_ra = {:.4} ms, tc_lp = {:.4} ms, tc_qp = {:.4} ms, tf_ra = {:.4}, tf_lp = {:.4}, tf_qp = {:.4}",
600            n_exp - succeed,
601            tc_sum_ra / succeed as f64,
602            tc_sum_lp / succeed as f64,
603            tc_sum_qp / succeed as f64,
604            tf_sum_ra / succeed as f64,
605            tf_sum_lp / succeed as f64,
606            tf_sum_qp / succeed as f64,
607        );
608
609        Ok(())
610    }
611}